Marcin Wardyński
Wtorek, 9:45
Uczenie Maszynowe - LAB2b - LIME¶
Biblioteka LIME: https://github.com/marcotcr/lime (Dokumentacja API: https://lime-ml.readthedocs.io/en/latest/)
Wprowadzenie - pakiety¶
Niezbędne pakiety i moduły na potrzeby wprowadzenia
import json
from functools import partial
import matplotlib.pyplot as plt
import numpy as np
import torch
from lime import lime_image
from PIL import Image
from skimage.segmentation import mark_boundaries
from torchvision import models, transforms
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Wprowadzenie - funkcje pomocnicze¶
Funkcja do wczytywania wskazanego obrazka oraz konwersji do palety RGB.
def get_image(path):
with open(path, 'rb') as f:
with Image.open(f) as img:
return img.convert('RGB')
Funkcja do przekształcania obrazka (zwróconego przez funkcję get_image) w tensor, akceptowalny na wejściu sieci neronowej.
def image_to_tensor(img):
transformer = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
return transformer(img).unsqueeze(0)
Funkcja używana przez LIME, przyjmuje na wejściu zbiór obrazków, a zwraca prawdopodobieństwa klas. Należy ją przekazać do lime_image.LimeImageExplainer().explain_instance przy użyciu partial, jako partial(predict_batch, <model>), gdzie modelem w naszym wypadku będą sieci neuronowe. Przykłady użycia są zawarte w tym notebooku.
def predict_batch(model, images):
model.eval()
transformer = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
model.eval()
batch = torch.stack(tuple(transformer(i) for i in images), dim=0)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
batch = batch.to(device)
logits = model(batch)
probas = torch.nn.functional.softmax(logits, dim=1)
return probas.detach().cpu().numpy()
Funkcja, która przekształca obrazek w format akceptowany na wejściu przez LIME. Przykłady użycia są zawarte w tym notebooku.
def lime_transformer(image):
transformer = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop(224)
])
return np.array(transformer(image))
LIME jest głównie wykorzystywane do wyjaśniania predykcji tzw. czarnych skrzynek, czyli modeli nieinterpretowalnych. Idealnymi kandydatami są Głębokie Sieci Neuronowe, dlatego spróbujemy wyjaśnić niektóre predykcje gotowych modeli.
Model Inception-v3 - przygotowanie danych¶
Plik ./data/imagenet_class_index.json zawiera przypisanie klas obrazków do indeksów. Jest to istotne, ponieważ zwracane wyniki (np. wartości funkcji logit na wyjściu sieci neuronowych) wykorzystują to, zwracając wyniki w zadanej kolejności.
with open("./data/imagenet_class_index.json") as f:
content = json.load(f)
index_to_label = {
int(index): data[1]
for index, data in content.items()
}
image_to_classify = get_image("./data/dogs.png")
plt.imshow(image_to_classify)
<matplotlib.image.AxesImage at 0x1299cc290>
img_tensor = image_to_tensor(image_to_classify)
Załadowanie pretrenowanego modelu¶
inception_v3 = models.inception_v3(pretrained=True)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. warnings.warn( /Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=Inception_V3_Weights.IMAGENET1K_V1`. You can also use `weights=Inception_V3_Weights.DEFAULT` to get the most up-to-date weights. warnings.warn(msg) Downloading: "https://download.pytorch.org/models/inception_v3_google-0cc3c7bd.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/inception_v3_google-0cc3c7bd.pth 100%|██████████| 104M/104M [00:01<00:00, 74.6MB/s]
Predykcja¶
inception_v3.eval()
logits = inception_v3(img_tensor)
Zwróć uwagę, że model zwraca wartości funkcji logit, a nie prawdopodobieństwa klas, dlatego wyniki trzeba przetworzyć (np. przy użyciu funkcji softmax).
probas = torch.nn.functional.softmax(logits, dim=1)
Sprawdźmy N najbardziej prawdopodobnych klas
TOP_N_LABELS = 15
probas_top = probas.topk(TOP_N_LABELS)
top_probas = probas_top[0][0].detach().numpy()
top_labels = probas_top[1][0].detach().numpy()
for proba, label in zip(top_probas, top_labels):
print(f"Class: {index_to_label[label]:<30} | Probability: {proba:.6f}")
Class: Bernese_mountain_dog | Probability: 0.935930 Class: EntleBucher | Probability: 0.038448 Class: Appenzeller | Probability: 0.023756 Class: Greater_Swiss_Mountain_dog | Probability: 0.001818 Class: Gordon_setter | Probability: 0.000009 Class: Blenheim_spaniel | Probability: 0.000007 Class: English_springer | Probability: 0.000002 Class: tabby | Probability: 0.000002 Class: robin | Probability: 0.000001 Class: guinea_pig | Probability: 0.000001 Class: amphibian | Probability: 0.000001 Class: Japanese_spaniel | Probability: 0.000001 Class: African_grey | Probability: 0.000001 Class: Brittany_spaniel | Probability: 0.000001 Class: toucan | Probability: 0.000001
Teraz możemy te funkcje zebrać razem¶
def get_prediction_probabilities(image, model):
img_tensor = image_to_tensor(image)
model.eval()
logits = model(img_tensor)
probas = torch.nn.functional.softmax(logits, dim=1)
TOP_N_LABELS = 15
probas_top = probas.topk(TOP_N_LABELS)
top_probas = probas_top[0][0].detach().numpy()
top_labels = probas_top[1][0].detach().numpy()
for proba, label in zip(top_probas, top_labels):
print(f"Class: {index_to_label[label]:<30} | Probability: {proba:.6f}")
I sprawdzić jak ta predykcja wygląda dla innego obrazka¶
exercise_image = get_image("./data/cat_mouse.jpeg")
plt.imshow(exercise_image)
<matplotlib.image.AxesImage at 0x12a90da90>
Zadanie: sprawdź jak będzie wyglądała predykcja dla powyższego obrazka¶
get_prediction_probabilities(exercise_image, inception_v3)
Class: Egyptian_cat | Probability: 0.967492 Class: tabby | Probability: 0.024167 Class: lynx | Probability: 0.005490 Class: tiger_cat | Probability: 0.002165 Class: Persian_cat | Probability: 0.000105 Class: Angora | Probability: 0.000074 Class: swab | Probability: 0.000071 Class: Madagascar_cat | Probability: 0.000064 Class: snow_leopard | Probability: 0.000040 Class: tile_roof | Probability: 0.000037 Class: indri | Probability: 0.000020 Class: leopard | Probability: 0.000016 Class: Siamese_cat | Probability: 0.000011 Class: ram | Probability: 0.000010 Class: crate | Probability: 0.000009
Model Inception-v3 - wyjaśnienie¶
Chcemy wiedzieć dlaczego klasa Bernese_mountain_dog została uznana przez sieć neuronową za najbardziej prawdopodobną (to znaczy - które piksele obrazka o tym zadecydowały). W tym celu właśnie wykorzystamy LIME.
W jaki sposób działa LIME na obrazkach?
- Na wejściu wymagany jest oryginalny obrazek.
- Wejściowy obrazek jest delikatnie przekształcany wiele razy, dzięki czemu otrzymujemy wiele podobnych (ale nie takich samych!) obrazków.
- Dodatkowo na wejście musimy podać funkcję, która każdemu takiemu przekształceniu nada prawdopodobieństwo przynależności do danej klasy. Jest to wymagane ponieważ LIME jest niezależny od żadnych narzędzi i modeli.
explainer = lime_image.LimeImageExplainer()
explanation = explainer.explain_instance(
image=lime_transformer(image_to_classify),
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
100%|██████████| 1000/1000 [00:27<00:00, 35.85it/s]
Mając te dane możemy teraz sprawdzić które kategorie są najbardziej prawdopodobne
for index in explanation.top_labels:
print(index_to_label[index])
Bernese_mountain_dog EntleBucher Appenzeller Greater_Swiss_Mountain_dog Gordon_setter
Zobaczmy co wpłynęło na wybranie Bernese_mountain_dog jako najbardziej prawdopodobnej klasy.
image, mask = explanation.get_image_and_mask(
label=explanation.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
<matplotlib.image.AxesImage at 0x12fb19130>
Zadanie: zmień wartość NUM_FEATURES i zaobserwuj jak zmienia się mapowanie¶
NUM_FEATURES najlepiej zmieniać w zakresie 1:50
NUM_FEATURES = 50
image, mask = explanation.get_image_and_mask(
label=explanation.top_labels[0],
positive_only=False,
negative_only=False,
num_features=NUM_FEATURES,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
<matplotlib.image.AxesImage at 0x12c255f40>
Zielone fragmenty oznaczają "superpiksele", które pozytywnie wpływają na predykowaną klasę. Czerwone fragmenty wpływają negatywnie.
Superpiksel to zbiór umieszczonych blisko siebie piskeli, które współdzielą pewne właściwości, jak np. kolor. Superpiksel nie ma odzwierciedlenia w pojedyńczym pikselu z obrazka, to piksele z obrazka mają odzwierciedlenie w superpikselu.
Zobaczmy jak to się prezentuje dla drugiej najbardziej prawdopodobnej klasy, czyli EntleBucher, która jednak otrzymała jedyne 3.8%.
image, mask = explanation.get_image_and_mask(
label=explanation.top_labels[1],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
<matplotlib.image.AxesImage at 0x12ab6deb0>
Ustawiając wartości hide_rest oraz positive_only na True jesteśmy w stanie zostawić tylko te piksele, które potwierdzały przynależność do danej klasy
Musimy jednak pamiętać o przeskalowaniu rezultatu przy pomocy (boundaries).astype(np.uint8)
image, mask = explanation.get_image_and_mask(
label=explanation.top_labels[0],
positive_only=True,
negative_only=False,
num_features=10,
hide_rest=True)
boundaries = mark_boundaries(image, mask)
plt.imshow((boundaries).astype(np.uint8))
<matplotlib.image.AxesImage at 0x12acd0fb0>
Możemy również zostawić tylko te piksele, które zaprzeczały przynależności do danej klasy
image, mask = explanation.get_image_and_mask(
label=explanation.top_labels[0],
positive_only=False,
negative_only=True,
num_features=10,
hide_rest=True)
boundaries = mark_boundaries(image, mask)
cropped_image_ndarray = (boundaries).astype(np.uint8)
plt.imshow(cropped_image_ndarray)
<matplotlib.image.AxesImage at 0x139f27980>
A następnie sprawdzić co model sądzi o tak wyciętym obrazku
cropped_image_pil = Image.fromarray(cropped_image_ndarray)
get_prediction_probabilities(cropped_image_pil, inception_v3)
Class: feather_boa | Probability: 0.459048 Class: groom | Probability: 0.445271 Class: mortarboard | Probability: 0.034262 Class: kimono | Probability: 0.014381 Class: bow_tie | Probability: 0.007375 Class: academic_gown | Probability: 0.005310 Class: abaya | Probability: 0.004244 Class: suit | Probability: 0.002887 Class: military_uniform | Probability: 0.001675 Class: grand_piano | Probability: 0.001089 Class: theater_curtain | Probability: 0.001022 Class: limousine | Probability: 0.000963 Class: gown | Probability: 0.000759 Class: cornet | Probability: 0.000723 Class: wig | Probability: 0.000655
I jak go teraz widzi model
cropped_image_explanation = explainer.explain_instance(
image=lime_transformer(cropped_image_pil),
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
100%|██████████| 1000/1000 [00:29<00:00, 34.18it/s]
image, mask = cropped_image_explanation.get_image_and_mask(
label=cropped_image_explanation.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
<matplotlib.image.AxesImage at 0x12ad831d0>
Model Inception-v3 - porównanie z AlexNet¶
Przetestujmy działanie na innym modelu - AlexNet
alexnet = models.alexnet(pretrained=True)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. warnings.warn( /Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=AlexNet_Weights.IMAGENET1K_V1`. You can also use `weights=AlexNet_Weights.DEFAULT` to get the most up-to-date weights. warnings.warn(msg) Downloading: "https://download.pytorch.org/models/alexnet-owt-7be5be79.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/alexnet-owt-7be5be79.pth 100%|██████████| 233M/233M [00:02<00:00, 101MB/s]
explanation_alexnet = explainer.explain_instance(
image=lime_transformer(image_to_classify),
classifier_fn=partial(predict_batch, alexnet),
top_labels=5,
num_samples=1000)
100%|██████████| 1000/1000 [00:09<00:00, 108.49it/s]
for index_alex, index_inception in zip(explanation_alexnet.top_labels, explanation.top_labels):
print(f"{index_to_label[index_alex]:30} | {index_to_label[index_inception]:30}")
Bernese_mountain_dog | Bernese_mountain_dog EntleBucher | EntleBucher Greater_Swiss_Mountain_dog | Appenzeller Appenzeller | Greater_Swiss_Mountain_dog basset | Gordon_setter
Jak widać, klasy nieco się różnią, ale TOP 1 pozostaje takie samo.
image, mask = explanation_alexnet.get_image_and_mask(
label=explanation_alexnet.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
<matplotlib.image.AxesImage at 0x12fd07ef0>
Wyjaśnienie dla AlexNet jak się można było spodziewać - też się różni, jednak w dalszym ciągu do klasyfikacji psa istotny jest... pies :)
Zadanie: porównaj predykcje obrazka dla modeli inception_v3 oraz alexnet¶
print("inception_v3")
get_prediction_probabilities(exercise_image, inception_v3)
print()
print("alexnet")
get_prediction_probabilities(exercise_image, alexnet)
inception_v3 Class: Egyptian_cat | Probability: 0.967492 Class: tabby | Probability: 0.024167 Class: lynx | Probability: 0.005490 Class: tiger_cat | Probability: 0.002165 Class: Persian_cat | Probability: 0.000105 Class: Angora | Probability: 0.000074 Class: swab | Probability: 0.000071 Class: Madagascar_cat | Probability: 0.000064 Class: snow_leopard | Probability: 0.000040 Class: tile_roof | Probability: 0.000037 Class: indri | Probability: 0.000020 Class: leopard | Probability: 0.000016 Class: Siamese_cat | Probability: 0.000011 Class: ram | Probability: 0.000010 Class: crate | Probability: 0.000009 alexnet Class: Persian_cat | Probability: 0.449892 Class: Egyptian_cat | Probability: 0.105569 Class: hamster | Probability: 0.075303 Class: lynx | Probability: 0.069438 Class: tiger_cat | Probability: 0.047823 Class: Angora | Probability: 0.043763 Class: tabby | Probability: 0.021709 Class: bucket | Probability: 0.020403 Class: plastic_bag | Probability: 0.013197 Class: coffee_mug | Probability: 0.012102 Class: tub | Probability: 0.011971 Class: hare | Probability: 0.008684 Class: hamper | Probability: 0.007576 Class: saltshaker | Probability: 0.005267 Class: wood_rabbit | Probability: 0.005078
Jak widzimy powyżej, dla obrazka ćwiczeniowego Alexnet oraz Inception_v3 proponują inne rozwiązania. Zaproponowany z niemalże pewnością przez Inception_v3 "Egyptian_cat" jest na miejscu drugim w Alexnet, ale ze znaczącą stratą do pierwszej klasy, czyli "Persian_cat", który to znowu jest dopiero na miejscu piątym w Inception_v3.
Zadanie domowe - wstęp¶
W folderze data znajduje się zdjęcie amfibii:

amphibious_vehicle = get_image("./data/amfibia.jpg")
inception_v3 = models.inception_v3(pretrained=True)
explanation_amhibious_vehicle_inception_v3 = explainer.explain_instance(
image=lime_transformer(amphibious_vehicle),
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
image, mask = explanation_amhibious_vehicle_inception_v3.get_image_and_mask(
label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. warnings.warn( /Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=Inception_V3_Weights.IMAGENET1K_V1`. You can also use `weights=Inception_V3_Weights.DEFAULT` to get the most up-to-date weights. warnings.warn(msg) 100%|██████████| 1000/1000 [00:29<00:00, 34.07it/s]
<matplotlib.image.AxesImage at 0x13853d310>
Model inception_v3 jak i jego wyjaśnienie rzeczywiście sugerują amfibię jako najbardziej prawdopodobną klasę:
for index in explanation_amhibious_vehicle_inception_v3.top_labels:
print(index_to_label[index])
amphibian convertible racer car_wheel golfcart
Zadanie #1¶
Użyj dwóch różnych sieci neuronowych (poza inception_v3, którego przykład jest powyżej) do wygenerowania wyjaśnień.
(skorzystaj z modułu torchvision: https://pytorch.org/vision/stable/models.html)
mobilenet_v3_l = models.mobilenet_v3_large(pretrained=True)
explanation_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
image=lime_transformer(amphibious_vehicle),
classifier_fn=partial(predict_batch, mobilenet_v3_l),
top_labels=5,
num_samples=1000)
image, mask = explanation_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
label=explanation_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. warnings.warn( /Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=MobileNet_V3_Large_Weights.IMAGENET1K_V1`. You can also use `weights=MobileNet_V3_Large_Weights.DEFAULT` to get the most up-to-date weights. warnings.warn(msg) Downloading: "https://download.pytorch.org/models/mobilenet_v3_large-8738ca79.pth" to /Users/mwardynski/.cache/torch/hub/checkpoints/mobilenet_v3_large-8738ca79.pth 100%|██████████| 21.1M/21.1M [00:00<00:00, 70.9MB/s] 100%|██████████| 1000/1000 [00:33<00:00, 29.70it/s]
<matplotlib.image.AxesImage at 0x13ac0e000>
for index in explanation_amhibious_vehicle_mobilenet_v3_l.top_labels:
print(index_to_label[index])
amphibian racer tow_truck convertible car_wheel
resnet50 = models.resnet50(pretrained=True)
explanation_amhibious_vehicle_resnet50 = explainer.explain_instance(
image=lime_transformer(amphibious_vehicle),
classifier_fn=partial(predict_batch, resnet50),
top_labels=5,
num_samples=1000)
image, mask = explanation_amhibious_vehicle_resnet50.get_image_and_mask(
label=explanation_amhibious_vehicle_resnet50.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
/Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. warnings.warn( /Users/mwardynski/Documents/ds/_semestr_9/uczenie_maszynowe/labs/.venv/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=ResNet50_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet50_Weights.DEFAULT` to get the most up-to-date weights. warnings.warn(msg) 100%|██████████| 1000/1000 [00:30<00:00, 32.62it/s]
<matplotlib.image.AxesImage at 0x12b865310>
for index in explanation_amhibious_vehicle_resnet50.top_labels:
print(index_to_label[index])
amphibian racer car_wheel sports_car pickup
Jak widzimy powyżej, każdy z modeli zaznacza trochę inny zestaw superpiskeli jako ten najbardziej naprowadzający na daną klasę pojazdu, jednakże widać dużą zbieżność pomiędzy superpikselami zaznaczonymi przez każdy z modeli.
Wszystkie modele skutecznie zaklasyfikowały pojazd jako amfibię.
Zadanie #2¶
Zmodyfikuj oryginalny obrazek w taki sposób, żeby najbardziej prawdopodobną klasą dla każdej z tych sieci nie była amfibia a jakiś inny pojazd (np. samochód). W tym celu możesz "zasłonić" czarnym kwadratem (wartość 0 w macierzy reprezentującej obraz) obszary istotne przy klasyfikacji.
Przydatną rzeczą będzie skorzystanie z opcji hide_rest w funkcji get_image_and_mask i późniejsza obróbka obrazu
from PIL import Image, ImageDraw
def calculate_rectagle_covering(mask):
width = mask.shape[0]
height = mask.shape[1]
min_x, min_y = width, height
max_x, max_y = 0, 0
for x in range(width):
for y in range(height):
px = mask[y][x]
if px == 1:
if x < min_x:
min_x = x
if y < min_y:
min_y = y
if x > max_x:
max_x = x
if y > max_y:
max_y = y
return (min_x, min_y), (max_x, max_y)
def cover_image_with_rectangle(mask, image):
rectangle = calculate_rectagle_covering(mask)
lime_image = lime_transformer(image)
lime_image[rectangle[0][1]:rectangle[1][1]+1, rectangle[0][0]:rectangle[1][0]+1] = [0, 0, 0]
return lime_image
num_features=1
image, mask = explanation_amhibious_vehicle_inception_v3.get_image_and_mask(
label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=True,
negative_only=False,
num_features=num_features,
hide_rest=True)
covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)
explanation_c_amhibious_vehicle_inception_v3 = explainer.explain_instance(
image=covered_lime_image,
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
image, mask = explanation_c_amhibious_vehicle_inception_v3.get_image_and_mask(
label=explanation_c_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
for index in explanation_c_amhibious_vehicle_inception_v3.top_labels:
print(index_to_label[index])
100%|██████████| 1000/1000 [00:28<00:00, 35.31it/s]
tow_truck amphibian pickup forklift harvester
num_features=1
image, mask = explanation_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=True,
negative_only=False,
num_features=num_features,
hide_rest=True)
covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)
explanation_c_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
image=covered_lime_image,
classifier_fn=partial(predict_batch, mobilenet_v3_l),
top_labels=5,
num_samples=1000)
image, mask = explanation_c_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
label=explanation_c_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
for index in explanation_c_amhibious_vehicle_mobilenet_v3_l.top_labels:
print(index_to_label[index])
100%|██████████| 1000/1000 [00:27<00:00, 35.90it/s]
tow_truck amphibian racer pickup tractor
num_features=3
image, mask = explanation_amhibious_vehicle_resnet50.get_image_and_mask(
label=explanation_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=True,
negative_only=False,
num_features=num_features,
hide_rest=True)
covered_lime_image = cover_image_with_rectangle(mask, amphibious_vehicle)
explanation_c_amhibious_vehicle_resnet50 = explainer.explain_instance(
image=covered_lime_image,
classifier_fn=partial(predict_batch, resnet50),
top_labels=5,
num_samples=1000)
image, mask = explanation_c_amhibious_vehicle_resnet50.get_image_and_mask(
label=explanation_c_amhibious_vehicle_resnet50.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
for index in explanation_c_amhibious_vehicle_resnet50.top_labels:
print(index_to_label[index])
100%|██████████| 1000/1000 [00:29<00:00, 33.61it/s]
wreck tractor pickup amphibian tow_truck
Widzimy powyżej, że wystarczyło przykryć czarnym prostokątem obszar zawierający najbardziej znaczący superpiksel przy klasyfikacji modelami InceptionV3 oraz MobileNetV3, żeby je zwieść i otrzymać klasyfikację "tow_truck"
Model ResNet wykazał się największą odpornością i dopiero po przykryciu trzech najistotniejszych dla klasyfikacji superpikseli, bez których to z pojazdu pozostają niemalże same koła, model zaklasyfikował pojazd do klasy: "wreck".
Zadanie #3¶
Ponownie zmodyfikuj oryginalny obraz, ale tym razem zaszumiając go w losowy sposób (przykładowa implementacja: https://www.geeksforgeeks.org/add-a-salt-and-pepper-noise-to-an-image-with-python/). Czy najbardziej prawdopodobna klasa zmienia się wraz ze zmianą szumu? Przetestuj dla każdego z modeli.
import random
import cv2
def add_noise(img):
# Getting the dimensions of the image
row , col = img.shape
# Randomly pick some pixels in the
# image for coloring them white
# Pick a random number between 300 and 10000
number_of_pixels = random.randint(300, 10000)
for i in range(number_of_pixels):
# Pick a random y coordinate
y_coord=random.randint(0, row - 1)
# Pick a random x coordinate
x_coord=random.randint(0, col - 1)
# Color that pixel to white
img[y_coord][x_coord] = 255
# Randomly pick some pixels in
# the image for coloring them black
# Pick a random number between 5000 and 10000
number_of_pixels = random.randint(5000 , 10000)
for i in range(number_of_pixels):
# Pick a random y coordinate
y_coord=random.randint(0, row - 1)
# Pick a random x coordinate
x_coord=random.randint(0, col - 1)
# Color that pixel to black
img[y_coord][x_coord] = 0
return img
# salt-and-pepper noise can
# be applied only to grayscale images
# Reading the color image in grayscale image
img = cv2.imread('./data/amfibia.jpg',
cv2.IMREAD_GRAYSCALE)
#Storing the image
cv2.imwrite('./data/sp_amfibia.jpg',
add_noise(img))
True
sp_amphibious_vehicle = get_image("./data/sp_amfibia.jpg")
explanation_sp_amhibious_vehicle_inception_v3 = explainer.explain_instance(
image=lime_transformer(sp_amphibious_vehicle),
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
image, mask = explanation_sp_amhibious_vehicle_inception_v3.get_image_and_mask(
label=explanation_sp_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:26<00:00, 37.10it/s]
<matplotlib.image.AxesImage at 0x36530aae0>
for index in explanation_sp_amhibious_vehicle_inception_v3.top_labels:
print(index_to_label[index])
amphibian jeep tow_truck car_wheel half_track
explanation_sp_amhibious_vehicle_mobilenet_v3_l = explainer.explain_instance(
image=lime_transformer(sp_amphibious_vehicle),
classifier_fn=partial(predict_batch, mobilenet_v3_l),
top_labels=5,
num_samples=1000)
image, mask = explanation_sp_amhibious_vehicle_mobilenet_v3_l.get_image_and_mask(
label=explanation_sp_amhibious_vehicle_mobilenet_v3_l.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:28<00:00, 35.45it/s]
<matplotlib.image.AxesImage at 0x3650748f0>
for index in explanation_sp_amhibious_vehicle_mobilenet_v3_l.top_labels:
print(index_to_label[index])
tow_truck amphibian snowplow racer jeep
explanation_sp_amhibious_vehicle_resnet50 = explainer.explain_instance(
image=lime_transformer(sp_amphibious_vehicle),
classifier_fn=partial(predict_batch, resnet50),
top_labels=5,
num_samples=1000)
image, mask = explanation_sp_amhibious_vehicle_resnet50.get_image_and_mask(
label=explanation_sp_amhibious_vehicle_resnet50.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:30<00:00, 32.62it/s]
<matplotlib.image.AxesImage at 0x3650bc8f0>
for index in explanation_sp_amhibious_vehicle_resnet50.top_labels:
print(index_to_label[index])
jeep amphibian Model_T pickup tow_truck
Po zaszumieniu oryginalnego obrazu możemy zauważyć, że tylko model InceptionV3 poradził sobie z właściwą klasyfikacją pojazdu. Model MobileNetV3 wskazał błędnie "tow_truck", natomiast ResNet-50 "jeep". Obydwa modele wskazały amfibię na drugim miejscu.
Słabszy wynik w przypadku MobileNet oraz ResNet nie powinien dziwić, gdyż są to modele słabiej klasyfikujące obiekty na obrazku, niż InceptionV3.
Poniżej przeprowadzę ponowne zaszumienie obrazu i zobaczymy, czy tym razem InceptionV3 sobie poradzi.
img = cv2.imread('./data/sp4_amfibia.jpg',
cv2.IMREAD_GRAYSCALE)
#Storing the image
cv2.imwrite('./data/sp5_amfibia.jpg',
add_noise(img))
True
sp_amphibious_vehicle = get_image("./data/sp5_amfibia.jpg")
explanation_sp_amhibious_vehicle_inception_v3 = explainer.explain_instance(
image=lime_transformer(sp_amphibious_vehicle),
classifier_fn=partial(predict_batch, inception_v3),
top_labels=5,
num_samples=1000)
image, mask = explanation_sp_amhibious_vehicle_inception_v3.get_image_and_mask(
label=explanation_sp_amhibious_vehicle_inception_v3.top_labels[0],
positive_only=False,
negative_only=False,
num_features=10,
hide_rest=False)
boundaries = mark_boundaries(image, mask)
plt.imshow(boundaries)
100%|██████████| 1000/1000 [00:27<00:00, 35.88it/s]
<matplotlib.image.AxesImage at 0x36593a7b0>
for index in explanation_sp_amhibious_vehicle_inception_v3.top_labels:
print(index_to_label[index])
amphibian half_track tow_truck pickup jeep
Dwukrotne zaszumienie obrazu nie pogorszyło jakości klasyfikacji modelu InceptionV3. Trzy- i czterokrotnie również nie. W tym momencie przerywam ten dodatkowy eksperyment. Ewidentnie InceptionV3 jako jeden z najmocniejszych modeli do rozpoznawania obiektów na obrazie radzi sobie z takim zaszumieniem perfekcyjnie.
Podsumowanie¶
Przedstawione powyżej przypadki w bardzo dobry sposób przedstawiają przydatność narzędzia LIME do analizowania istotności superpikseli dla jakości klasyfikacji.
Analiza ta pozwala w kontrolowany sposób porównać ze sobą różne modele i ujawnić ich mocne oraz słabe strony.
Przede wszystki narzędzie pokazuje "ludzką" stronę modeli, gdyż widząc relewantne superpiksele, sami możemy przytaknąć, że w duże mierze faktycznie pokrywają się z cechami obiektów, po których my jako ludzie rozróżniamy przedmioty.